#!/usr/bin/python
"""
Nirvanix REST API.
(C)2007 Enomaly Inc.
License: MIT
"""
from time import time
from httplib2 import Http
from urllib import quote,unquote,urlencode
from nv_exceptions import *
from xml.dom.minidom import parse, parseString
from formutil import encode_multipart_formdata
from os.path import dirname,basename

urlbase="https://services.nirvanix.com/ws/"

class connection:
    """
    Class which is a base connection to nirvanix's web services
    """
    def __init__(self,user,passwd,appkey=None,appname=None):
        """
        @param user: The user account to connect as
        @param passwd: The user password to authenticate with
        @param appkey: The application key for the provided appname
        @param appname: The application name to work in
        """
        if not appkey:
            raise nv_bad_appkey_err("No appkey provided.")
        if not appname:
            raise nv_bad_appkey_err("No appname provided.")
        self.conn=Http()
        self.user,self.passwd=user,passwd
        self.appkey=appkey        
        self.appname=appname
        self.token=0
        
    def _request(self,path,*args,**kw):
        """
        Base method which connects to nirvanix for a ws call. Has
        rudimentary error handling included.
        @param path: The path of the service to call. 
        @return: dict of response headers, dom of response
        """
        resp,content=self.conn.request(path,*args,**kw)
        #print content
        if resp.status!=200:
            #print content
            raise nv_application_err("Error connecting to server:"+str(resp.status))
        try:
            dom=parseString(content)
        except Exception,e:
            #print content
            raise nv_application_err("XML Parsing Error:"+str(e.message))
        try:
            rcode=dom.getElementsByTagName('ResponseCode')[0].childNodes[0].data.strip()
        except Exception,e:
            raise nv_application_err("XML Parsing Error:"+str(e.message))
        if int(rcode)==80001:
            raise nv_authentication_err("Invalid credentials")
        elif int(rcode)!=0:
            try:
                msg=nv_errcodes[int(rcode)]
            except:
                msg="undefined error"
            #print content
            raise nv_application_err(\
                "Error calling %s due to: %s"%(path,msg),int(rcode))
        if rcode==0:
            raise Exception
            
        return resp,dom
        
    def _call(self,path,method,vars,body=None,**kw):
        """
        Calls _request with the given params, but does some more processing
        to decide how to handle input data.
        @param path: The path of the service to call
        @param method: GET/POST/HEAD etc...
        @param vars: Parameters to pass to the call
        @param body: The request body. Overrides vars
        @return: dict of response headers, dom of response
        """
        if type(vars)==dict:
            vars=urlencode(vars)
        body=None
        if path[0:4]=="http":
            url=path            
        elif body and not (method=="GET" or method=="HEAD"):
            url=urlbase+path
            body=vars
        elif method=="GET" or method=="HEAD":
            getvars="?"+vars
            url=urlbase+path+getvars
        else:
            url=urlbase+path
            body=vars
        #print "URL:",url
        return self._request(url,method,body=body,**kw)
    
        
    def call(self,path,method="GET",vars=None,body=None,**kw):
        """
        Quick little wrapper for the nirvanix rest API calls
        Calls _call with the given params, but also ensures that there is
        a live session to connect with.
        @param path: The path of the service to call
        @param method: GET/POST/HEAD etc...
        @param vars: Parameters to pass to the call
        @param body: The request body. Overrides vars
        @return: dict of response headers, dom of response
        """
        token=self.get_auth()
        if not body and type(vars)==dict and vars.has_key('sessionToken'):
            vars['sessionToken']=token 
        try:
            return self._call(path,method,vars,body,**kw)
        except :
            self.authenticate()
            return self._call(path,method,vars,body,**kw    )
        
    def authenticate(self):
        """
        Auth with the nirvanix web services
        @return: Current session key
        """
        self.token=0
        vars={'appKey':self.appkey,\
        'userName':self.user,\
        'password':self.passwd}
        resp,dom=self._call("Authentication/Login.ashx","GET",vars)
        try:
            self.token=dom.getElementsByTagName('SessionToken')[0].childNodes[0].data.strip()
        except Exception,e:
            raise nv_response_err('Invalid response contents: Missing token.'+str(e))
        self.auth_time=time()
        #print "SEFLTOKEN:",self.token
        return self.token
        
        
    def get_auth(self):
        """
        Ensures that our authorization has not expired. 
        Called for nearly all methods. 
        @return: dict of response headers, dom of response
        """
        if self.token and (self.auth_time+(60*18))>time():
            #print "OKTOKEN"
            return self.token
        else:
            #print "NEWTOKEN"
            return self.authenticate()
            
    def logout(self):
        """
        Logs the user out. Kills session key.
        """
        resp,dom=self._call("Authentication/Logout.ashx","GET",{'sessionToken':self.token})
        self.token=None
            
        
        
class filesystem(connection):
    def get_download_node(self,path):
        """
        Get the server from which we will download the item
        @param path: The path of the requested file
        @return: The server hostname/IP to connect to for download
        """
        token=self.get_auth()
        #print "TOKENGDN:",token
        vars={'sessionToken':token,'filePath':path}        
        resp,dom=self.call("IMFS/GetDownloadNodes.ashx","GET",vars)
        try:
            node=dom.getElementsByTagName('DownloadNode')[0].childNodes[0].data.strip()
        except Exception,e:
            raise nv_response_err('Invalid response contents: Missing token. '+str(e))
        return node
        
    def get_upload_node(self,size=0):
        """
        Get the server to which we will upload an item
        @param size: The size of the uploading file in bytes
        @return: The IP address of the upload server, the upload token string
        """
        token=self.get_auth()
        vars={'sessionToken':token,'sizeBytes':int(size)}
        resp,dom=self.call("IMFS/GetUploadNode.ashx","GET",vars)
        try:
            node=dom.getElementsByTagName('IPAddress')[0].childNodes[0].data.strip()
            uptoken=dom.getElementsByTagName('AccessToken')[0].childNodes[0].data.strip()
        except Exception,e:
            raise nv_response_err('Invalid response contents: Missing token. '+str(e))
        return node,uptoken
        
        
    def set_file_from_string(self,path,data,**kw):
        """
        Sets the contents of a file from a string. Overwrites an existing 
        file if it is there, otherwise creates it.
        
        @param path: The file to save
        @param data: The contents as a string
        @return: The response data as a dom tree (minidom)
        """
        node,uptoken=self.get_upload_node(len(data))
        #print "Uploading to %s with token %s"%(node,uptoken)
        filepath=dirname(path)
        filename=basename(path)
        fields=[\
                ('uploadToken',uptoken),\
                ('destFolderPath',filepath),\
                ('submit','Upload')\
            ]
        files=(('uploadFile1',filename,data),)
        if kw.has_key('mimetype'):
            mimetype=kw['mimetype']
        else:
            mimetype=None #Auto
        content_type,body=encode_multipart_formdata(fields, files, mimetype)
        #print body
        headers={\
            'Content-Type':content_type,\
            'Content-Length':str(len(body))
        }
        #We can shortcut since we JUST got authed and tokenified.
        if self.file_exists(path):
            #print "File exists!"
            self.delete_file(path)
        try:
            resp,dom=self._request("http://"+node+"/Upload.ashx","POST",body=body,headers=headers)
        except Exception,e:
            #print "Error saving data.",e
            raise e
        return dom
        
        
    def file_exists(self,path):
        """
        Returns T/F if a file exists
        @param path: The file to check
        @return: True/False 1/0
        """
        token=self.get_auth()
        filepath=dirname(path)
        filename=basename(path)
        try:
            node=self.get_download_node(path)
        except nv_application_err,e:
            #print e,dir(e),e.message,e.__dict__
            if e.errorcode==70005:
                return 0
            else:
                raise e
        return 1

        
    def delete_file(self,path):
        """
        Deletes a file at the given path.
        @param path: The file to delete
        """
        token=self.get_auth()
        filepath=dirname(path)
        filename=basename(path)
        vars={'sessionToken':token,'filePath':path}
        resp,dom=self.call("IMFS/DeleteFiles.ashx","GET",vars)
        
        
    def delete_folder(self,path):
        """
        Deletes a folder and all of it's contents
        """
        pass
        
    def get_string_from_file(self,path,node=None,method="https",**kw):
        """
        Gets the file and places it in a string.
        kwargs include... 
            - accept: mimetype
            
        @param path: The file to retrieve
        @param node: The hostname to download from, if we know it already
        @param method: "http" or "https"
        """
        token=self.get_auth()
        vars={}        
        headers={}
        if kw.has_key('accept'):
            headers['accept']=kw['accept']
        if not node:
            node=self.get_download_node(path)
        url=method+"://"+node+"/"+quote(token)+"/"+quote(self.appname)+"/"+quote(self.user)+"/"+path
        resp,data=self.conn.request(url,"GET",vars,headers=headers)
        return data
        
    


class accountmgr(connection):
    
    def CreateChildAccount(self,**kw):
        """
        Kw must contain the following structures:
        Parameter       Description                           Details
        =============================================================
        SessionToken    A valid session 
                        token for the user.             - Required
        Username        The account username.           - Required
                                                        - Length limit: 100
        Password        The account password.           - Required
                                                        - Length limit: 
                                                          6 to 100
        FirstName       An account contact name         - Optional
                                                        - Length limit: 30
        LastName                                        - Optional
                                                        - Length limit: 30
        MiddleInitial                                   - Optional
                                                        - Length limit: 1
        PhoneNumber     A primary contact phone number  - Optional
                                                        - Length limit: 50
        EmailAddress    The Email address associated    - Optional
                        with the account.               - Length limit: 1000
        EmailFormat     The format the email will be    - Optional
                        sent to the account. (html/text)
        Address1                                        - Optional
                                                        - Length limit: 200
        Address2                                        - Length limit: 200
        City                                            - Optional
                                                        - Length limit: 200
        State                                           - Optional
                                                        - Length limit: 2
        CountryID                                       - Optional
        PostalCode                                      - Optional
                                                        - Length limit: 50
        """
        pass    
        
    
    
if __name__=="__main__":
    #Dumb test
    import random
    foo=filesystem('elasticdrive','PASSWORD','APPLICATION_UUID_GOES_HERE','ElasticDrive')
    print foo.authenticate()
    data_in="THis is a test file"+chr(random.randint(ord("A"),ord("Z")))*5
    print foo.set_file_from_string("foo/baz/test/filenamex",data_in)
    bar = foo.get_string_from_file("foo/baz/test/filenamex")
    print "BAR IS:",bar
    assert bar==data_in
    #print foo.logout()
